package io.gitee.wsitm.export;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.hutool.poi.excel.cell.CellUtil;
import io.gitee.wsitm.exception.ClutterIOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.Row;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 数据导出工具
 *
 * @author lzy
 * @date 2021/9/23 9:34
 */
public class ExportKit {

    /**
     * 导出 excel ，不适合导出大文件
     *
     * @param response 响应对象
     * @param list     数据
     */
    public static <T> void exportXlsx(HttpServletResponse response, List<T> list) {
        String curTime = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
        exportXlsx(response, String.format("数据-%s.xlsx", curTime), list);
    }

    /**
     * 导出 excel ，不适合导出大文件
     *
     * @param response 响应对象
     * @param filename 文件名
     * @param list     数据
     */
    public static <T> void exportXlsx(HttpServletResponse response, String filename, List<T> list) {
        try {
            filename = URLEncoder.encode(filename, "UTF-8");
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + filename);
            exportXlsx(response.getOutputStream(), list);
        } catch (Exception e) {
            throw new ClutterIOException("导出异常", e);
        }
    }

    /**
     * 导出 excel ，不适合导出大文件
     *
     * @param file 文件
     * @param list 数据
     */
    public static <T> void exportXlsx(File file, List<T> list) {
        OutputStream outputStream = FileUtil.getOutputStream(file);
        exportXlsx(outputStream, list);
    }

    /**
     * 导出 excel ，不适合导出大文件
     *
     * @param outputStream 输出流
     * @param list         数据
     */
    public static <T> void exportXlsx(OutputStream outputStream, List<T> list) {
        try (ExcelWriter excelWriter = ExcelUtil.getBigWriter()) {
            exportXlsx(excelWriter, list);
            excelWriter.flush(outputStream, true);
        } finally {
            IoUtil.close(outputStream);
        }
    }

    /**
     * 导出 excel ，不适合导出大文件
     *
     * @param excelWriter excel写入对象
     * @param list        数据
     */
    public static <T> void exportXlsx(ExcelWriter excelWriter, List<T> list) {
        if (CollUtil.isNotEmpty(list)) {
            try {
                //图片的列下标
                List<Column> columnList = getColumnList(list.get(0));
                int rowCnt = columnList.get(0).getFullHeaders().length;
                for (int i = 0; i < list.size(); i++) {
                    if (i == 0) {
                        fullHeader(excelWriter, columnList);
                    }

                    Map<String, Object> map = BeanUtil.beanToMap(list.get(i), new LinkedHashMap<>(), false, false);
                    for (int j = 0; j < columnList.size(); j++) {
                        Column column = columnList.get(j);
                        Object obj = map.get(column.getKey());
                        if (CollUtil.isNotEmpty(column.getConverterMap())) {
                            // 进行翻译
                            obj = column.getConverterMap().get(Objects.toString(obj));
                        }

                        //时间格式化
                        if (StrUtil.isNotEmpty(column.getDateFormat())) {
                            obj = formatDateTime(obj, column.getDateFormat());
                        }

                        if (obj instanceof Number && column.getPercent() > -1) {
                            obj = String.format("%." + column.getPercent() + "f%%", Convert.toDouble(obj) * 100);
                        }

                        excelWriter.writeCellValue(j, i + rowCnt,
                                column.getPrefix() + obj + column.getSuffix());
                    }
                }
            } catch (Exception e) {
                throw new ClutterIOException("导出异常", e);
            }
        }
    }

    /**
     * 设置单元格
     *
     * @param excelWriter excel写入对象
     * @param x           x
     * @param y           x
     * @param text        内容
     * @param isHeader    头
     */
    public static void setCell(ExcelWriter excelWriter, int x, int y, Object text, boolean isHeader) {
        Row row = excelWriter.getOrCreateRow(y);
        Cell cell = CellUtil.getOrCreateCell(row, x);
        CellUtil.setCellValue(cell, text, excelWriter.getStyleSet(), isHeader);
    }

    /**
     * 填充表头
     *
     * @param writer     excel写入对象
     * @param columnList 列
     */
    private static void fullHeader(ExcelWriter writer, List<Column> columnList) {
        List<Merge> mergeList = new ArrayList<>();
        for (int i = 0; i < columnList.size(); i++) {
            String[] fullHeaders = columnList.get(i).getFullHeaders();
            int width = columnList.get(i).getWidth();
            if (width <= 0) {
                width = fullHeaders.length > 0 && StrUtil.isNotBlank(fullHeaders[0]) ? fullHeaders[0].length() * 3 : 15;
            }
            writer.setColumnWidth(i, width);

            if (!columnList.get(i).getClazz().isAssignableFrom(Number.class)) {
                CellStyle cellStyle = writer.getCellStyle();
                DataFormat dataFormat = writer.getWorkbook().createDataFormat();
                cellStyle.setDataFormat(dataFormat.getFormat("@"));
                writer.setColumnStyle(i, cellStyle);
            }

            for (int j = 0; j < fullHeaders.length; j++) {
                // 逐个插入表头
                ExportKit.setCell(writer, i, j, fullHeaders[j], true);
                ExportKit.setCell(writer, i, j, fullHeaders[j], true);

                // 判断获取合并的坐标
                if (CollUtil.isEmpty(mergeList)) {
                    mergeList.add(new Merge(fullHeaders[j], i, j));
                } else {
                    final int x = i, y = j;
                    Optional<Boolean> optionalBoolean = mergeList.stream()
                            .map(merge -> merge.merge(fullHeaders[y], x, y))
                            .filter(Boolean::booleanValue)
                            .findFirst();
                    if (!optionalBoolean.isPresent()) {
                        mergeList.add(new Merge(fullHeaders[j], i, j));
                    }
                }
            }
        }

        // 合并表头
        for (Merge merge : mergeList) {
            if (merge.isMerge()) {
                writer.merge(merge.getStartRowIndex(), merge.getEndRowIndex(), merge.getStartColIndex(), merge.getEndColIndex(), null, true);
            }
        }
        // 跳到非表头行
        if (CollUtil.isNotEmpty(columnList)) {
            writer.setCurrentRow(columnList.get(0).getFullHeaders().length);
        }

    }

    // --------------------------------------------------------------------------------------------------------------------

    /**
     * 导出CSV文件，使用分段式，减少内存的使用，大量数据时建议使用该方式
     *
     * @param response 响应对象
     * @param consumer 消费者，流式回调
     */
    public static <T> void exportCsv(HttpServletResponse response,
                                     Consumer<Function<List<T>, Integer>> consumer) {
        String curTime = DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN);
        exportCsv(response, String.format("数据-%s.xlsx", curTime), consumer);
    }

    /**
     * 导出CSV文件，使用分段式，减少内存的使用，大量数据时建议使用该方式
     *
     * @param response 响应对象
     * @param filename 文件名，默认拼接当前时间，和文件后缀
     * @param consumer 消费者，流式回调
     */
    public static <T> void exportCsv(HttpServletResponse response, String filename,
                                     Consumer<Function<List<T>, Integer>> consumer) {
        try (
                OutputStream outputStream = response.getOutputStream()
        ) {
            filename = URLEncoder.encode(filename, "UTF-8");
            response.setContentType("application/octet-stream;charset=utf-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + filename);
            exportCsv(response.getOutputStream(), consumer);
            outputStream.flush();
        } catch (Exception e) {
            throw new ClutterIOException("导出异常", e);
        }
    }

    /**
     * 导出CSV文件，使用分段式，减少内存的使用，大量数据时建议使用该方式
     *
     * @param dirPath  目录路径
     * @param fileName 文件名
     * @param consumer 消费者，流式回调
     * @return 文件
     */
    public static <T> File exportCsv(String dirPath, String fileName,
                                     Consumer<Function<List<T>, Integer>> consumer) {
        File file = FileUtil.file(dirPath, fileName);
        exportCsv(file, consumer);
        return file;
    }

    /**
     * 导出CSV文件，使用分段式，减少内存的使用，大量数据时建议使用该方式
     *
     * @param file     导出文件
     * @param consumer 消费者，流式回调
     * @throws ClutterIOException 异常
     */
    public static <T> void exportCsv(File file, Consumer<Function<List<T>, Integer>> consumer) {
        OutputStream outputStream = FileUtil.getOutputStream(file);
        exportCsv(outputStream, consumer);
    }


    /**
     * 导出CSV文件，使用分段式，减少内存的使用，大量数据时建议使用该方式
     *
     * @param outputStream 输出流
     * @param consumer     消费者，流式回调
     */
    public static <T> void exportCsv(OutputStream outputStream,
                                     Consumer<Function<List<T>, Integer>> consumer) {
        try {
            AtomicBoolean hasHeader = new AtomicBoolean(false);
            consumer.accept(list -> {
                if (CollUtil.isNotEmpty(list)) {
                    try {
                        List<Column> columnList = getColumnList(list.get(0));

                        StringBuilder stringBuilder = new StringBuilder();
                        for (T item : list) {
                            if (!hasHeader.get()) {
                                String strHeader = columnList.stream()
                                        .map(column -> String.format("\"%s\"\t", ArrayUtil.join(column.getHeaders(), "-")))
                                        .collect(Collectors.joining(","));
                                outputStream.write((strHeader + "\r\n").getBytes("gbk"));
                                outputStream.flush();
                                if (item instanceof Map) {
                                    // 如果是map数据类型，必须在第一行加上标题行
                                    continue;
                                }
                                hasHeader.set(true);
                            }

                            Map<String, Object> map = BeanUtil.beanToMap(item, new LinkedHashMap<>(), false, false);
                            String strLine = columnList.stream()
                                    .map(column -> {
                                        Object val = map.get(column.getKey());
                                        if (CollUtil.isNotEmpty(column.getConverterMap())) {
                                            // 进行翻译
                                            val = column.getConverterMap().get(Objects.toString(val));
                                        }

                                        //时间格式化
                                        if (StrUtil.isNotEmpty(column.getDateFormat())) {
                                            val = formatDateTime(val, column.getDateFormat());
                                        }

                                        if (column.getHandlerIns() != null && column.getHandlerMethod() != null) {
                                            val = dataFormatHandlerAdapter(column, val, item);
                                        }

                                        if (val instanceof Number) {
                                            if (column.getPercent() > -1) {
                                                String format = "%." + column.getPercent() + "f%%";
                                                val = String.format(format, Convert.toDouble(val) * 100);
                                            } else {
                                                return String.format("%s\t", val);
                                            }
                                        }
                                        return String.format("\"%s\"\t", column.getPrefix() + val + column.getSuffix());
                                    })
                                    .collect(Collectors.joining(","));
                            stringBuilder.append(strLine).append("\r\n");
                        }

                        outputStream.write(stringBuilder.toString().getBytes("gbk"));
                        outputStream.flush();

                        return list.size();
                    } catch (Exception e) {
                        throw new ClutterIOException("导出异常", e);
                    }
                }
                return 0;
            });
        } finally {
            IoUtil.close(outputStream);
        }
    }

    // --------------------------------------------------------------------------------------------------------------------

    /**
     * 通过对象获取列，根据 @Header 判断
     *
     * @param object 对象
     * @return 列
     */
    private static List<Column> getColumnList(Object object)
            throws InstantiationException, IllegalAccessException, NoSuchMethodException {
        int heardRowCnt = 1; // 表头的行数
        List<Column> columnList = new ArrayList<>();

        if (object instanceof List) {
            throw new ClutterIOException("元素对象不能是List");
        } else if (object instanceof Map) {
            Map map = (Map) object;
            Set<Object> keys = map.keySet();

            int order = 0;
            for (Object key : keys) {
                columnList.add(
                        new Column(StrUtil.toString(key), map.get(key).getClass(), ++order,
                                new String[]{StrUtil.toString(map.get(key))}, 0));
            }
            return columnList;
        } else {
            Class<?> beanClass = ClassUtil.getClass(object);
            // 获取所有字段
            Field[] fields = ReflectUtil.getFields(beanClass, field -> field.getAnnotation(Header.class) != null);
            for (Field field : fields) {
                Header header = field.getAnnotation(Header.class);
                Column column = new Column(field.getName(), field.getType(),
                        header.sort(), header.value(), header.width());

                if (StrUtil.isNotEmpty(header.dateFormat())) {
                    column.setDateFormat(header.dateFormat());
                }

                if (StrUtil.isNotEmpty(header.converterExp())) {
                    List<String> expList = StrUtil.split(header.converterExp(), ",");
                    if (CollUtil.isNotEmpty(expList)) {
                        Map<String, String> expMap = expList.stream()
                                .map(str -> {
                                    List<String> keyVal = StrUtil.split(str, "=");
                                    if (CollUtil.isNotEmpty(keyVal) && keyVal.size() > 1) {
                                        return keyVal;
                                    }
                                    return null;
                                })
                                .filter(list -> CollUtil.isNotEmpty(list) && StrUtil.isNotEmpty(list.get(0)))
                                .collect(Collectors.toMap(
                                        list -> list.get(0).trim(),
                                        list -> list.get(1).trim(),
                                        (m1, m2) -> m2
                                ));
                        column.setConverterMap(expMap);
                    }
                }

                if (!header.handler().equals(HandlerAdapter.class)) {
                    column.setHandlerIns(header.handler().newInstance());
                    column.setHandlerMethod(header.handler().getMethod("format", new Class[]{Object.class, beanClass}));
                }

                column.setPrefix(header.prefix());
                column.setSuffix(header.suffix());
                column.setPercent(header.percent());

                columnList.add(column);
                if (header.value().length > heardRowCnt) {
                    heardRowCnt = header.value().length;
                }
            }
        }

        // 处理表头，进行排序
        int finalRowCnt = heardRowCnt;
        columnList = columnList.stream()
                .map(column -> {
                    String[] headers = column.getHeaders();
                    if (headers.length != finalRowCnt) {
                        String[] newHeaders = new String[finalRowCnt];
                        System.arraycopy(headers, 0, newHeaders, 0, headers.length);
                        for (int i = headers.length; i < finalRowCnt; i++) {
                            newHeaders[i] = headers[headers.length - 1];
                        }
                        column.setFullHeaders(newHeaders);
                    } else {
                        column.setFullHeaders(headers);
                    }
                    return column;
                })
                .sorted(Comparator.comparingInt(Column::getSort))
                .collect(Collectors.toList());

        return columnList;
    }

    // -------------------------------------------------------------------------------

    /**
     * 数据处理器
     *
     * @param value 数据值
     * @return 处理后的数据
     */
    private static <T> Object dataFormatHandlerAdapter(Column column, Object value, T item) {
        try {
            return column.getHandlerMethod().invoke(column.getHandlerIns(), value, item);
        } catch (Exception e) {
            throw new ClutterIOException("不能格式化数据" + column.getHandlerIns(), e.getMessage());
        }
    }

    /**
     * 格式化时间
     *
     * @param obj    值
     * @param format 格式
     * @return 格式后时间
     */
    private static String formatDateTime(Object obj, String format) {
        if (obj instanceof Date) {
            return DateUtil.format((Date) obj, format);
        } else if (obj instanceof LocalDateTime) {
            return DateUtil.format((LocalDateTime) obj, format);
        } else if (obj instanceof LocalDate) {
            return DateUtil.format(((LocalDate) obj).atStartOfDay(), format);
        } else if (obj instanceof String) {
            return DateUtil.parse(obj.toString()).toString(format);
        } else if (obj instanceof Long) {
            return DateUtil.date((long) obj).toString(format);
        }
        return StrUtil.toString(obj);
    }


}
